#region Copyright
/// <copyright>
/// Copyright (c) 2007 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Collections.Generic;
using ComTypes = System.Runtime.InteropServices.ComTypes;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

using Osh;
using Osh.Scripting;

namespace OshHost
{
	public class WebBrowserEx : System.Windows.Forms.WebBrowser
	{
		#region WebBrowserSiteEx Class
		protected class WebBrowserSiteEx : System.Windows.Forms.WebBrowser.WebBrowserSite,
			NativeInterfaces.IDocHostUIHandler,
			NativeInterfaces.IServiceProvider,
			NativeInterfaces.IInternetSecurityManager,
			NativeInterfaces.IOleCommandTarget
		{
			private static Guid IID_IInternetSecurityManager = 
				Marshal.GenerateGuidForType(typeof(NativeInterfaces.IInternetSecurityManager));
			private static string OSHSecurityId = "osh:";
			private const int OSHSecurityIdSize = 4;
			private const int OSHSecurityZone = 3;
			private const int OSHSecurityIdAndZoneSize = 4 + sizeof(int);

			private WebBrowserEx _webBrowser;

			public WebBrowserSiteEx(WebBrowserEx webBrowser) : base(webBrowser)
			{
				_webBrowser = webBrowser;
			}

			#region IDocHostUIHandler Members
			int NativeInterfaces.IDocHostUIHandler.ShowContextMenu(int dwID, Win32.RECT pt, object pcmdtReserved, object pdispReserved)
			{
				if(_webBrowser.IsWebBrowserContextMenuEnabled)
				    return Win32.S_FALSE;
				else
				    return Win32.S_OK;
			}

			int NativeInterfaces.IDocHostUIHandler.TranslateAccelerator(ref Win32.MSG msg, ref Guid group, int nCmdID)
			{
				bool? result = _webBrowser.OnTranslateAccelerator(msg);
				if(result != null && result.Value)
				    return Win32.S_OK;

				if(_webBrowser.WebBrowserShortcutsEnabled)
				    return Win32.S_FALSE;

				if(msg.message == Win32.WM_CHAR)
				    return Win32.S_FALSE;

				Shortcut shortcut = (Shortcut)(msg.wParam.ToInt32() | (int)Control.ModifierKeys);

				if(!Enum.IsDefined(typeof(Shortcut), shortcut) ||
				    _webBrowser.EnabledShortcuts.Contains(shortcut))
				    return Win32.S_FALSE;

				return Win32.S_OK;
			}

			int NativeInterfaces.IDocHostUIHandler.GetExternal(out object ppDispatch)
			{
				ppDispatch = null; //_webBrowser.ObjectForScripting;
				return Win32.S_OK;
			}

			int NativeInterfaces.IDocHostUIHandler.GetHostInfo(Win32.DOCHOSTUIINFO info)
			{
				info.dwDoubleClick = 0;
				info.dwFlags = (int)
					(Win32.DOCHOSTUIFLAG.DOCHOSTUIFLAG_DISABLE_SCRIPT_INACTIVE |
					Win32.DOCHOSTUIFLAG.DOCHOSTUIFLAG_DIV_BLOCKDEFAULT |
					Win32.DOCHOSTUIFLAG.DOCHOSTUIFLAG_FLAT_SCROLLBAR |
					Win32.DOCHOSTUIFLAG.DOCHOSTUIFLAG_NO3DBORDER |
					Win32.DOCHOSTUIFLAG.DOCHOSTUIFLAG_THEME);
				info.dwDoubleClick = (int)(Win32.DOCHOSTUIDBLCLK.DOCHOSTUIDBLCLK_DEFAULT);
				return Win32.S_OK;
			}

			int NativeInterfaces.IDocHostUIHandler.ShowUI(int dwID, IntPtr activeObject, IntPtr commandTarget, IntPtr frame, IntPtr doc)
			{
				return Win32.S_FALSE;
			}

			int NativeInterfaces.IDocHostUIHandler.HideUI()
			{
				return Win32.E_NOTIMPL;
			}

			int NativeInterfaces.IDocHostUIHandler.UpdateUI()
			{
				return Win32.E_NOTIMPL;
			}

			int NativeInterfaces.IDocHostUIHandler.EnableModeless(bool fEnable)
			{
				return Win32.E_NOTIMPL;
			}

			int NativeInterfaces.IDocHostUIHandler.OnDocWindowActivate(bool fActivate)
			{
				return Win32.E_NOTIMPL;
			}

			int NativeInterfaces.IDocHostUIHandler.OnFrameWindowActivate(bool fActivate)
			{
				return Win32.E_NOTIMPL;
			}

			int NativeInterfaces.IDocHostUIHandler.ResizeBorder(Win32.COMRECT rect, IntPtr doc, bool fFrameWindow)
			{
				return Win32.E_NOTIMPL;
			}

			int NativeInterfaces.IDocHostUIHandler.GetOptionKeyPath(string[] pbstrKey, int dw)
			{
				return Win32.E_NOTIMPL;
			}

			int NativeInterfaces.IDocHostUIHandler.GetDropTarget(IntPtr pDropTarget, out IntPtr ppDropTarget)
			{
				ppDropTarget = IntPtr.Zero;
				return Win32.E_NOTIMPL;
			}

			int NativeInterfaces.IDocHostUIHandler.TranslateUrl(int dwTranslate, string strURLIn, out string pstrURLOut)
			{
				pstrURLOut = null;
				return Win32.S_FALSE;
			}

			int NativeInterfaces.IDocHostUIHandler.FilterDataObject(ComTypes.IDataObject pDO, out ComTypes.IDataObject ppDORet)
			{
				ppDORet = null;
				return Win32.S_FALSE;
			}
			#endregion

			#region IServiceProvider Members
			public int QueryService(ref Guid guidService, ref Guid riid, out IntPtr ppvObject)
			{
				if(guidService == IID_IInternetSecurityManager &&
					riid == IID_IInternetSecurityManager)
				{
					ppvObject = Marshal.GetComInterfaceForObject(this,
						typeof(NativeInterfaces.IInternetSecurityManager));
					return Win32.S_OK;
				}
				ppvObject = IntPtr.Zero;
				return Win32.E_NOINTERFACE;
			}
			#endregion IServiceProvider Members

			#region IInternetSecurityManager Members
			public unsafe int SetSecuritySite(void* pSite)
			{
			    return Win32.INET_E_DEFAULT_ACTION;
			}

			public unsafe int GetSecuritySite(void** ppSite)
			{
			    return Win32.INET_E_DEFAULT_ACTION;
			}

			public unsafe int MapUrlToZone(string url, int* pdwZone, int dwFlags)
			{
				if(!IsOshSchema(url))
				    return Win32.INET_E_DEFAULT_ACTION;

				if(pdwZone == null)
					return Win32.E_POINTER;

				*pdwZone = OSHSecurityZone;
				return Win32.S_OK;
			}

			public unsafe int GetSecurityId(string url, byte* pbSecurityId, int* pcbSecurityId, int dwReserved)
			{
				if(!IsOshSchema(url))
				    return Win32.INET_E_DEFAULT_ACTION;

				if(*pcbSecurityId < OSHSecurityIdAndZoneSize)
				{
					*pcbSecurityId = OSHSecurityIdAndZoneSize;
					return Win32.S_FALSE;
				}

				// Copying OSHSecurityId and OSHSecurityZone
				for(int i = 0; i < OSHSecurityIdSize; i++)
					pbSecurityId[i] = (byte)OSHSecurityId[i];
				*(int*)(&pbSecurityId[OSHSecurityIdSize]) = OSHSecurityZone;

				*pcbSecurityId = OSHSecurityIdAndZoneSize;
				return Win32.S_OK;
			}

			public unsafe int ProcessUrlAction(string url, int dwAction, byte* pPolicy, int cbPolicy,
				byte* pContext, int cbContext, int dwFlags, int dwReserved)
			{
				if(!IsOshSchema(url))
				    return Win32.INET_E_DEFAULT_ACTION;

				if(dwAction == (int)Win32.UrlAction.URLACTION_SCRIPT_MIN ||
					dwAction == (int)Win32.UrlAction.URLACTION_SCRIPT_OVERRIDE_SAFETY ||
					dwAction == (int)Win32.UrlAction.URLACTION_CROSS_DOMAIN_DATA)
				{
					if(cbPolicy < sizeof(uint))
						return Win32.S_FALSE;

					*((int*)pPolicy) = (int)Win32.UrlPolicy.URLPOLICY_ALLOW;
					return Win32.S_OK;
				}


			    return Win32.INET_E_DEFAULT_ACTION;
			}

			public unsafe int QueryCustomPolicy(string pwszUrl, void* guidKey, byte** ppPolicy, int* pcbPolicy, byte* pContext, int cbContext, int dwReserved)
			{
			    return Win32.INET_E_DEFAULT_ACTION;
			}

			public int SetZoneMapping(int dwZone, string lpszPattern, int dwFlags)
			{
			    return Win32.INET_E_DEFAULT_ACTION;
			}

			public unsafe int GetZoneMappings(int dwZone, void** ppenumString, int dwFlags)
			{
			    return Win32.INET_E_DEFAULT_ACTION;
			}
			#endregion

			#region IOleCommandTarget Members
			int NativeInterfaces.IOleCommandTarget.QueryStatus(IntPtr pguidCmdGroup, int cCmds, IntPtr prgCmds, IntPtr pCmdText)
			{
				return Win32.OLECMDERR_E_UNKNOWNGROUP;
			}

			int NativeInterfaces.IOleCommandTarget.Exec(IntPtr pguidCmdGroup, int nCmdID, int nCmdexecopt, object[] pvaIn, object[] pvaOut)
			{
				if(pguidCmdGroup == IntPtr.Zero)
					return Win32.E_POINTER;

				Guid cmdGroup = (Guid)Marshal.PtrToStructure(pguidCmdGroup, typeof(Guid));

				if(cmdGroup != Win32.CGID_DocHostCommandHandler)
					return Win32.OLECMDERR_E_UNKNOWNGROUP;

				if(nCmdID != (int)Win32.OLECMDID.OLECMDID_SHOWSCRIPTERROR)
					return Win32.OLECMDERR_E_NOTSUPPORTED;

				if(pvaIn == null || pvaOut == null)
					return Win32.E_POINTER;
				if(pvaIn.Length < 1 || pvaOut.Length < 1)
					return Win32.E_UNEXPECTED;

				NativeInterfaces.IHTMLDocument2 doc = null;
				NativeInterfaces.IHTMLWindow2 win = null;
				NativeInterfaces.IHTMLEventObj eventObj = null;
				bool continueRunningScript = true;
				try
				{
					doc = (pvaIn[0] as NativeInterfaces.IHTMLDocument2);
					if(doc == null)
						return Win32.E_UNEXPECTED;

					win = (doc.ParentWindow as NativeInterfaces.IHTMLWindow2);
					if(win == null)
						return Win32.E_UNEXPECTED;

					eventObj = win.Event;
					if(eventObj == null)
						return Win32.E_UNEXPECTED;

					Type eventObjType = typeof(NativeInterfaces.IHTMLEventObj);
					int errorLine = (int)eventObjType.InvokeMember("errorLine", System.Reflection.BindingFlags.GetProperty, null, eventObj, null);
					int errorCharacter = (int)eventObjType.InvokeMember("errorCharacter", System.Reflection.BindingFlags.GetProperty, null, eventObj, null);
					int errorCode = (int)eventObjType.InvokeMember("errorCode", System.Reflection.BindingFlags.GetProperty, null, eventObj, null);
					string errorMessage = (string)eventObjType.InvokeMember("errorMessage", System.Reflection.BindingFlags.GetProperty, null, eventObj, null);
					string errorUrl = (string)eventObjType.InvokeMember("errorUrl", System.Reflection.BindingFlags.GetProperty, null, eventObj, null);
					continueRunningScript =
						_webBrowser.OnScriptError(new ScriptError(errorLine, errorCharacter, errorCode, errorMessage, errorUrl));
				}
				catch
				{
					return Win32.E_FAIL;
				}
				finally
				{
					if(doc != null) Marshal.ReleaseComObject(doc);
					if(win != null) Marshal.ReleaseComObject(win);
					if(eventObj != null) Marshal.ReleaseComObject(eventObj);
				}

				pvaOut[0] = continueRunningScript;
				return Win32.S_OK;
			}
			#endregion IOleCommandTarget Members
		
			private bool IsOshSchema(string url)
			{
				return !string.IsNullOrEmpty(url) &&
					url.StartsWith(OSHSecurityId, StringComparison.InvariantCultureIgnoreCase);
			}
		}
		#endregion WebBrowserSiteEx Class

		private delegate object InvokeScriptHandler(string scriptName, object[] args);
		private List<Shortcut> _enabledShortcuts = new List<Shortcut>();
		private WebBrowserSiteEx _site;
		private bool _uiHandlerSet;
		private bool _disposed;

		public WebBrowserEx()
		{
		}

		protected override void Dispose(bool disposing)
		{
			_disposed = true;
			base.Dispose(disposing);
		}

		protected virtual bool OnScriptError(ScriptError error)
		{
			if(this.ScriptError == null)
				return true;

			ScriptErrorEventArgs eventArgs = new ScriptErrorEventArgs(error);
			this.ScriptError(this, eventArgs);
			return eventArgs.ContinueRunningScript;
		}

		protected override void OnNavigated(WebBrowserNavigatedEventArgs e)
		{
			if(!_uiHandlerSet)
			    _uiHandlerSet = TrySetUIHandler();

			base.OnNavigated(e);
		}

		private bool TrySetUIHandler()
		{
			NativeInterfaces.IWebBrowser2 webBrowser2 = this.ActiveXInstance as NativeInterfaces.IWebBrowser2;
			if(webBrowser2 == null)
				return false;

			NativeInterfaces.ICustomDoc document = webBrowser2.Document as NativeInterfaces.ICustomDoc;
			if(document == null)
				return false;

			document.SetUIHandler((WebBrowserSiteEx)CreateWebBrowserSiteBase());
			return true;
		}

		protected override WebBrowserSiteBase CreateWebBrowserSiteBase()
		{
			if(_site == null)
				_site = new WebBrowserSiteEx(this);
			return _site;
		}

		public List<Shortcut> EnabledShortcuts
		{
			get { return _enabledShortcuts; }
		}

		public bool? OnTranslateAccelerator(Win32.MSG msg)
		{
			if(this.TranslateAccelerator != null)
			{
				TranslateAcceleratorEventArgs args = new TranslateAcceleratorEventArgs(msg);
				this.TranslateAccelerator(this, args);
				return args.Handled;
			}
			return null;
		}

		public object InvokeScript(string scriptName)
		{
			return InvokeScript(scriptName, null);
		}

		public object InvokeScript(string scriptName, params object[] args)
		{
			if(_disposed)
				return null;

			if(this.InvokeRequired)
				return this.Invoke(new InvokeScriptHandler(InvokeScriptInternal), scriptName, args);
			else
				return InvokeScriptInternal(scriptName, args);
		}

		private object InvokeScriptInternal(string scriptName, object[] args)
		{
			if(this.Document == null)
				throw new InvalidOperationException("HtmlDocument is null.");

			return this.Document.InvokeScript(scriptName, args);
		}

		public object GetFrameDocument(object frame)
		{
			NativeInterfaces.IHTMLDOMNode2 frameNode = frame as NativeInterfaces.IHTMLDOMNode2;
			if(frameNode == null)
				throw new ArgumentOutOfRangeException("frame");

			NativeInterfaces.IHTMLDocument2 doc = frameNode.OwnerDocument as NativeInterfaces.IHTMLDocument2;

			NativeInterfaces.IOleContainer container = doc as NativeInterfaces.IOleContainer;
			NativeInterfaces.IEnumUnknown unkEnum;
			int hr = container.EnumObjects((int)Win32.OLECONTF.OLECONTF_EMBEDDINGS, out unkEnum);
			Marshal.ThrowExceptionForHR(hr);

			IntPtr frameUnknown = Marshal.GetIUnknownForObject(frame);
			object result = null;
			IntPtr unkPtr;
			int fetched;
			while(unkEnum.Next(1, out unkPtr, out fetched) == Win32.S_OK)
			{
				if(unkPtr == frameUnknown)
				{
					object obj = Marshal.GetObjectForIUnknown(unkPtr);
					Marshal.Release(unkPtr);

					NativeInterfaces.IWebBrowser2 browser = obj as NativeInterfaces.IWebBrowser2;
					if(browser != null)
						result = browser.Document;

					Marshal.ReleaseComObject(obj);
					break;
				}
			}
			if(unkEnum != null)
				Marshal.ReleaseComObject(unkEnum);
			Marshal.Release(frameUnknown);

			return result;
		}

		public event EventHandler<TranslateAcceleratorEventArgs> TranslateAccelerator;
		public event EventHandler<ScriptErrorEventArgs> ScriptError;
	}
}
